﻿////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <copyright>Copyright 2008-2011 Andreas Huber Doenni</copyright>
// Distributed under the GNU General Public License version 2 (GPLv2).
// See accompanying file License.txt or copy at http://phuse.codeplex.com/license.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

namespace Phuse.Mime
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.CodeAnalysis;
    using System.IO;
    using System.Linq;

    using Phuse.IO;

    /// <summary>The common base class of all entities (see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>
    /// 2.4.).</summary>
    /// <threadsafety static="true" instance="false"/>
    public abstract class Entity : IVisitable<Entity>
    {
        private readonly HeaderFieldCollection headerFields = new HeaderFieldCollection();
        private Body body;

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>Gets the Date field (see <a href="http://www.ietf.org/rfc/rfc5322.txt">RFC 5322</a> 3.6.1.)
        /// of the entity.</summary>
        /// <value>An object representing the field, if the entity has such a field; otherwise <c>null</c>.</value>
        public DateHeaderField DateField
        {
            get { return this.GetField<DateHeaderField>(HeaderFieldNames.Date); }
        }

        /// <summary>Gets or sets the body of the Date field converted to UTC (see
        /// <a href="http://www.ietf.org/rfc/rfc5322.txt">RFC 5322</a> 3.6.1.) of the entity.</summary>
        /// <value>An object representing the body of the field, if the entity has such a field; otherwise <c>null</c>.
        /// </value>
        public DateTime? Date
        {
            get { return GetFieldBody(this.DateField); }
            set { this.SetFieldBody(HeaderFieldNames.Date, value); }
        }

        /// <summary>Gets the From field (see <a href="http://www.ietf.org/rfc/rfc5322.txt">RFC 5322</a> 3.6.2.)
        /// of the entity.</summary>
        /// <value>An object representing the field, if the entity has such a field; otherwise <c>null</c>.</value>
        public AddressListHeaderField FromField
        {
            get { return this.GetField<AddressListHeaderField>(HeaderFieldNames.From); }
        }

        /// <summary>Gets or sets the body of the From field (see <a href="http://www.ietf.org/rfc/rfc5322.txt">RFC
        /// 5322</a> 3.6.2.) of the entity.</summary>
        /// <value>An object representing the body of the field, if the entity has such a field; otherwise <c>null</c>.
        /// </value>
        /// <remarks>This property deviates from common expectations as follows:
        /// <list type="bullet">
        /// <item>The returned collection object is <c>null</c> by default</item>
        /// <item>There is a setter, which can be used to set <c>null</c> or an object implementing the
        /// <see cref="IList{T}"/> interface. The former removes any possibly present From fields from the header, the
        /// latter ensures the presence of a single From field containing all the addresses in the list.</item>
        /// </list>
        /// Both are necessary to keep usage consistent with other (non-collection) properties.</remarks>
        [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "The contents of a set list is copied.")]
        public IList<Address> From
        {
            get { return GetFieldBody(this.FromField); }
            set { this.SetFieldBody(HeaderFieldNames.From, value); }
        }

        /// <summary>Gets the Sender field (see <a href="http://www.ietf.org/rfc/rfc5322.txt">RFC 5322</a> 3.6.2.)
        /// of the entity.</summary>
        /// <value>An object representing the field, if the entity has such a field; otherwise <c>null</c>.</value>
        public AddressListHeaderField SenderField
        {
            get { return this.GetField<AddressListHeaderField>(HeaderFieldNames.Sender); }
        }

        /// <summary>Gets or sets the body of the Sender field (see <a href="http://www.ietf.org/rfc/rfc5322.txt">RFC
        /// 5322</a> 3.6.2.) of the entity.</summary>
        /// <value>An object representing the body of the field, if the entity has such a field; otherwise <c>null</c>.
        /// </value>
        /// <remarks>This property deviates from common expectations in the same way as the <see cref="From"/> property
        /// does. See there fore details.</remarks>
        [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "The contents of a set list is copied.")]
        public IList<Address> Sender
        {
            get { return GetFieldBody(this.SenderField); }
            set { this.SetFieldBody(HeaderFieldNames.Sender, value); }
        }

        /// <summary>Gets the Reply-To field (see <a href="http://www.ietf.org/rfc/rfc5322.txt">RFC 5322</a> 3.6.2.)
        /// of the entity.</summary>
        /// <value>An object representing the field, if the entity has such a field; otherwise <c>null</c>.</value>
        public AddressListHeaderField ReplyToField
        {
            get { return this.GetField<AddressListHeaderField>(HeaderFieldNames.ReplyTo); }
        }

        /// <summary>Gets or sets the body of the Reply-To field (see <a href="http://www.ietf.org/rfc/rfc5322.txt">RFC
        /// 5322</a> 3.6.2.) of the entity.</summary>
        /// <value>An object representing the body of the field, if the entity has such a field; otherwise <c>null</c>.
        /// </value>
        /// <remarks>This property deviates from common expectations in the same way as the <see cref="From"/> property
        /// does. See there fore details.</remarks>
        [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "The contents of a set list is copied.")]
        public IList<Address> ReplyTo
        {
            get { return GetFieldBody(this.ReplyToField); }
            set { this.SetFieldBody(HeaderFieldNames.ReplyTo, value); }
        }

        /// <summary>Gets the To field (see <a href="http://www.ietf.org/rfc/rfc5322.txt">RFC 5322</a> 3.6.3.)
        /// of the entity.</summary>
        /// <value>An object representing the field, if the entity has such a field; otherwise <c>null</c>.</value>
        public AddressListHeaderField ToField
        {
            get { return this.GetField<AddressListHeaderField>(HeaderFieldNames.To); }
        }

        /// <summary>Gets or sets the body of the To field (see <a href="http://www.ietf.org/rfc/rfc5322.txt">RFC
        /// 5322</a> 3.6.3.) of the entity.</summary>
        /// <value>An object representing the body of the field, if the entity has such a field; otherwise <c>null</c>.
        /// </value>
        /// <remarks>This property deviates from common expectations in the same way as the <see cref="From"/> property
        /// does. See there fore details.</remarks>
        [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "The contents of a set list is copied.")]
        public IList<Address> To
        {
            get { return GetFieldBody(this.ToField); }
            set { this.SetFieldBody(HeaderFieldNames.To, value); }
        }

        /// <summary>Gets the Cc field (see <a href="http://www.ietf.org/rfc/rfc5322.txt">RFC 5322</a> 3.6.3.)
        /// of the entity.</summary>
        /// <value>An object representing the field, if the entity has such a field; otherwise <c>null</c>.</value>
        [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Cc", Justification = "Cc is the official casing in RFC 5322.")]
        public AddressListHeaderField CcField
        {
            get { return this.GetField<AddressListHeaderField>(HeaderFieldNames.Cc); }
        }

        /// <summary>Gets or sets the body of the Cc field (see <a href="http://www.ietf.org/rfc/rfc5322.txt">RFC
        /// 5322</a> 3.6.3.) of the entity.</summary>
        /// <value>An object representing the body of the field, if the entity has such a field; otherwise <c>null</c>.
        /// </value>
        /// <remarks>This property deviates from common expectations in the same way as the <see cref="From"/> property
        /// does. See there fore details.</remarks>
        [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "The contents of a set list is copied.")]
        [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Cc", Justification = "Cc is the official casing in RFC 5322.")]
        public IList<Address> Cc
        {
            get { return GetFieldBody(this.CcField); }
            set { this.SetFieldBody(HeaderFieldNames.Cc, value); }
        }

        /// <summary>Gets the Bcc field (see <a href="http://www.ietf.org/rfc/rfc5322.txt">RFC 5322</a> 3.6.3.)
        /// of the entity.</summary>
        /// <value>An object representing the field, if the entity has such a field; otherwise <c>null</c>.</value>
        public AddressListHeaderField BccField
        {
            get { return this.GetField<AddressListHeaderField>(HeaderFieldNames.Bcc); }
        }

        /// <summary>Gets or sets the body of the Bcc field (see <a href="http://www.ietf.org/rfc/rfc5322.txt">RFC
        /// 5322</a> 3.6.3.) of the entity.</summary>
        /// <value>An object representing the body of the field, if the entity has such a field; otherwise <c>null</c>.
        /// </value>
        /// <remarks>This property deviates from common expectations in the same way as the <see cref="From"/> property
        /// does. See there fore details.</remarks>
        [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "The contents of a set list is copied.")]
        public IList<Address> Bcc
        {
            get { return GetFieldBody(this.BccField); }
            set { this.SetFieldBody(HeaderFieldNames.Bcc, value); }
        }

        /// <summary>Gets the Message-ID field (see <a href="http://www.ietf.org/rfc/rfc5322.txt">RFC 5322</a> 3.6.4.)
        /// of the entity.</summary>
        /// <value>An object representing the field, if the entity has such a field; otherwise <c>null</c>.</value>
        public UnstructuredHeaderField MessageIdField
        {
            get { return this.GetField<UnstructuredHeaderField>(HeaderFieldNames.MessageId); }
        }

        /// <summary>Gets or sets the body of the Message-ID field (see
        /// <a href="http://www.ietf.org/rfc/rfc5322.txt">RFC 5322</a> 3.6.4.) of the entity.</summary>
        /// <value>An object representing the body of the field, if the entity has such a field; otherwise <c>null</c>.
        /// </value>
        public string MessageId
        {
            get { return GetFieldBody(this.MessageIdField); }
            set { this.SetFieldBody(HeaderFieldNames.MessageId, value); }
        }

        /// <summary>Gets the In-Reply-To field (see <a href="http://www.ietf.org/rfc/rfc5322.txt">RFC 5322</a> 3.6.4.)
        /// of the entity.</summary>
        /// <value>An object representing the field, if the entity has such a field; otherwise <c>null</c>.</value>
        public ReferencesHeaderField InReplyToField
        {
            get { return this.GetField<ReferencesHeaderField>(HeaderFieldNames.InReplyTo); }
        }

        /// <summary>Gets or sets the body of the In-Reply-To field (see
        /// <a href="http://www.ietf.org/rfc/rfc5322.txt">RFC 5322</a> 3.6.4.) of the entity.</summary>
        /// <value>An object representing the body of the field, if the entity has such a field; otherwise <c>null</c>.
        /// </value>
        /// <remarks>This property deviates from common expectations in the same way as the <see cref="From"/> property
        /// does. See there fore details.</remarks>
        [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "The contents of a set list is copied.")]
        public IList<string> InReplyTo
        {
            get { return GetFieldBody(this.InReplyToField); }
            set { this.SetFieldBody(HeaderFieldNames.InReplyTo, value); }
        }

        /// <summary>Gets the References field (see <a href="http://www.ietf.org/rfc/rfc5322.txt">RFC 5322</a> 3.6.4.)
        /// of the entity.</summary>
        /// <value>An object representing the field, if the entity has such a field; otherwise <c>null</c>.</value>
        public ReferencesHeaderField ReferencesField
        {
            get { return this.GetField<ReferencesHeaderField>(HeaderFieldNames.References); }
        }

        /// <summary>Gets or sets the body of the References field (see
        /// <a href="http://www.ietf.org/rfc/rfc5322.txt">RFC 5322</a> 3.6.4.) of the entity.</summary>
        /// <value>An object representing the body of the field, if the entity has such a field; otherwise <c>null</c>.
        /// </value>
        /// <remarks>This property deviates from common expectations in the same way as the <see cref="From"/> property
        /// does. See there fore details.</remarks>
        [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "The contents of a set list is copied.")]
        public IList<string> References
        {
            get { return GetFieldBody(this.ReferencesField); }
            set { this.SetFieldBody(HeaderFieldNames.References, value); }
        }

        /// <summary>Gets the Subject field (see <a href="http://www.ietf.org/rfc/rfc5322.txt">RFC 5322</a> 3.6.5.) of
        /// the entity.</summary>
        /// <value>An object representing the field, if the entity has such a field; otherwise <c>null</c>.</value>
        public UnstructuredHeaderField SubjectField
        {
            get { return this.GetField<UnstructuredHeaderField>(HeaderFieldNames.Subject); }
        }

        /// <summary>Gets the body of the Subject field (see <a href="http://www.ietf.org/rfc/rfc5322.txt">RFC 5322</a>
        /// 3.6.5.) of the entity.</summary>
        /// <value>An object representing the body of the field, if the entity has such a field; otherwise <c>null</c>.
        /// </value>
        public string Subject
        {
            get { return GetFieldBody(this.SubjectField); }
            set { this.SetFieldBody(HeaderFieldNames.Subject, value); }
        }

        /// <summary>Gets the Comments field (see <a href="http://www.ietf.org/rfc/rfc5322.txt">RFC 5322</a> 3.6.5.) of
        /// the entity.</summary>
        /// <value>An object representing the field, if the entity has such a field; otherwise <c>null</c>.</value>
        public UnstructuredHeaderField CommentsField
        {
            get { return this.GetField<UnstructuredHeaderField>(HeaderFieldNames.Comments); }
        }

        /// <summary>Gets the body of the Comments field (see <a href="http://www.ietf.org/rfc/rfc5322.txt">RFC 5322</a>
        /// 3.6.5.) of the entity.</summary>
        /// <value>An object representing the body of the field, if the entity has such a field; otherwise <c>null</c>.
        /// </value>
        public string Comments
        {
            get { return GetFieldBody(this.CommentsField); }
            set { this.SetFieldBody(HeaderFieldNames.Comments, value); }
        }

        /// <summary>Gets the Keywords field (see <a href="http://www.ietf.org/rfc/rfc5322.txt">RFC 5322</a> 3.6.5.) of
        /// the entity.</summary>
        /// <value>An object representing the field, if the entity has such a field; otherwise <c>null</c>.</value>
        public UnstructuredHeaderField KeywordsField
        {
            get { return this.GetField<UnstructuredHeaderField>(HeaderFieldNames.Keywords); }
        }

        /// <summary>Gets the body of the Keywords field (see <a href="http://www.ietf.org/rfc/rfc5322.txt">RFC 5322</a>
        /// 3.6.5.) of the entity.</summary>
        /// <value>An object representing the body of the field, if the entity has such a field; otherwise <c>null</c>.
        /// </value>
        public string Keywords
        {
            get { return GetFieldBody(this.KeywordsField); }
            set { this.SetFieldBody(HeaderFieldNames.Keywords, value); }
        }

        /// <summary>Gets the Content-Type field (see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a> 5.) of
        /// the entity.</summary>
        /// <value>An object representing the field, if the entity has such a field; otherwise <c>null</c>.</value>
        public ParameterizedHeaderField ContentTypeField
        {
            get { return this.GetField<ParameterizedHeaderField>(HeaderFieldNames.ContentType); }
        }

        /// <summary>Gets the Content-Transfer-Encoding field (see
        /// <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a> 6.) of the entity.</summary>
        /// <value>An object representing the field, if the entity has such a field; otherwise <c>null</c>.</value>
        public ParameterizedHeaderField ContentTransferEncodingField
        {
            get { return this.GetField<ParameterizedHeaderField>(HeaderFieldNames.ContentTransferEncoding); }
        }

        /// <summary>Gets the Content-Disposition field (see <a href="http://www.ietf.org/rfc/rfc2183.txt">RFC 2183</a>,
        /// 2.) of the entity.</summary>
        /// <value>An object representing the field, if the entity has such a field; otherwise <c>null</c>.</value>
        public ParameterizedHeaderField ContentDispositionField
        {
            get { return this.GetField<ParameterizedHeaderField>(HeaderFieldNames.ContentDisposition); }
        }

        /// <summary>Gets a collection that allows direct case-insensitive access to the header fields.</summary>
        /// <remarks>
        /// <para><strong>Caution</strong>: The collection obtained through this property allows for absolute control
        /// over the header. For example, it is possible to add multiple From fields although
        /// <a href="http://www.ietf.org/rfc/rfc5322.txt">RFC 5322</a> 3.6. states that there cannot be more than one
        /// such field in any header. Similarly, it is possible to remove fields that are required to be present.
        /// Finally, it is possible to add fields of incorrect type. E.g. a <see cref="DateHeaderField"/> object can be
        /// added with the name <see cref="HeaderFieldNames.From"/>.</para>
        /// <para>All direct access properties, like e.g. <see cref="DateField"/>, <see cref="Date"/>, etc. only ever
        /// return the first field with a matching name. <c>null</c> is returned, if no field is found or the first
        /// field does not have the expected type.</para>
        /// </remarks>
        public HeaderFieldCollection Fields
        {
            get { return this.headerFields; }
        }

        /// <summary>Gets the body of the entity or <c>null</c> if the entity has no body.</summary>
        public Body Body
        {
            get { return this.body; }
            set { this.body = value; }
        }

        /// <summary>See <see cref="IVisitable{T}.Accept"/>.</summary>
        public abstract void Accept(IVisitor<Entity> visitor);

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        internal Entity()
        {
        }

        internal void WriteToImpl(Stream headerStream, Stream bodyStream)
        {
            Action<IEnumerable<HeaderField>, bool> writeHeaderFields = (fields, writeEmptyLine) =>
                this.WriteHeaderFields(headerStream, fields, (bodyStream != null) && writeEmptyLine);

            if (this.body != null)
            {
                this.body.WriteTo(bodyStream, writeHeaderFields);
            }
            else
            {
                writeHeaderFields(Enumerable.Empty<HeaderField>(), false);
            }
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        private TField GetField<TField>(string name) where TField : HeaderField<TField>
        {
            return this.Fields[name].FirstOrDefault() as TField;
        }

        private void SetFieldBody(string name, DateTime? value)
        {
            this.SetFieldBody(name, value, n => new DateHeaderField(n), (f, d) => f.DateUtc = (DateTime)d);
        }

        private void SetFieldBody(string name, IList<Address> value)
        {
            this.SetFieldBody(name, value, n => new AddressListHeaderField(n), (f, a) => CopyList(a, f.Addresses));
        }

        private void SetFieldBody(string name, string value)
        {
            this.SetFieldBody(name, value, n => new UnstructuredHeaderField(n), (f, b) => f.Body = b);
        }

        private void SetFieldBody(string name, IList<string> value)
        {
            this.SetFieldBody(name, value, n => new ReferencesHeaderField(n), (f, r) => CopyList(r, f.References));
        }

        private void WriteHeaderFields(Stream stream, IEnumerable<HeaderField> requiredHeaderFields, bool writeEmptyLine)
        {
            var requiredList = requiredHeaderFields.ToList();

            foreach (var grouping in this.headerFields)
            {
                var requiredIndex = FindIndex(requiredList, grouping.Key);

                if (requiredIndex >= 0)
                {
                    requiredList[requiredIndex].WriteTo(stream);
                    requiredList.RemoveAt(requiredIndex);
                }
                else
                {
                    foreach (var field in grouping)
                    {
                        field.WriteTo(stream);
                    }
                }
            }

            foreach (var field in requiredList)
            {
                field.WriteTo(stream);
            }

            if (writeEmptyLine)
            {
                StreamHelper.WriteLineBreak(stream);
            }
        }

        private void SetFieldBody<TField, TValue>(
            string name, TValue value, Func<string, TField> createField, Action<TField, TValue> setField)
            where TField : HeaderField<TField>
        {
            if (value == null)
            {
                this.headerFields.Remove(name);
            }
            else
            {
                setField(this.GetOrCreateField<TField>(name, createField), value);
            }
        }

        private TField GetOrCreateField<TField>(string name, Func<string, TField> createField)
            where TField : HeaderField<TField>
        {
            var field = this.GetField<TField>(name);

            if (field == null)
            {
                field = createField(name);
                this.headerFields.Add(field);
            }

            return field;
        }

        private static DateTime? GetFieldBody(DateHeaderField field)
        {
            return GetFieldBody<DateHeaderField, DateTime?>(field, f => f.DateUtc);
        }

        private static IList<Address> GetFieldBody(AddressListHeaderField field)
        {
            return GetFieldBody(field, f => f.Addresses);
        }

        private static string GetFieldBody(UnstructuredHeaderField field)
        {
            return GetFieldBody(field, f => f.Body);
        }

        private static IList<string> GetFieldBody(ReferencesHeaderField field)
        {
            return GetFieldBody(field, f => f.References);
        }

        private static TValue GetFieldBody<TField, TValue>(TField field, Func<TField, TValue> getField)
            where TField : HeaderField<TField>
        {
            return field == null ? default(TValue) : getField(field);
        }

        private static void CopyList<T>(IList<T> source, IList<T> destination)
        {
            destination.Clear();

            foreach (var value in source)
            {
                destination.Add(value);
            }
        }

        private static int FindIndex(List<HeaderField> fields, string name)
        {
            for (int index = 0; index < fields.Count; ++index)
            {
                if (fields[index].Name.Equals(name, StringComparison.OrdinalIgnoreCase))
                {
                    return index;
                }
            }

            return -1;
        }
    }
}
